<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>星辰宇宙 - JavaScript 3D 粒子系统</title>
  <style>
    body {
      overflow: hidden;
      margin: 0;
      background: #000;
      font-family: Arial, sans-serif;
    }

    .info {
      position: absolute;
      top: 10px;
      left: 10px;
      color: white;
      z-index: 100;
      background: rgba(0,0,0,0.7);
      padding: 10px;
      border-radius: 5px;
      font-size: 14px;
    }

    .controls {
      position: absolute;
      top: 10px;
      right: 10px;
      color: white;
      z-index: 100;
      background: rgba(0,0,0,0.7);
      padding: 10px;
      border-radius: 5px;
      font-size: 12px;
    }

    .footer {
      position: absolute;
      bottom: 10px;
      left: 50%;
      transform: translateX(-50%);
      color: white;
      z-index: 100;
      background: rgba(0,0,0,0.7);
      padding: 5px 15px;
      border-radius: 15px;
      font-size: 12px;
      text-align: center;
    }

    .footer a {
      color: #4CAF50;
      text-decoration: none;
    }

    .footer a:hover {
      text-decoration: underline;
    }
  </style>
</head>
<body>
<div class="info">
  <h3>🌌 星辰宇宙</h3>
  <p>粒子数量: 150,000</p>
  <p>技术栈: Three.js + WebGL</p>
  <p>状态: <span id="status">加载中...</span></p>
</div>

<div class="controls">
  <h4>🎮 控制说明</h4>
  <p>🖱️ 鼠标拖拽: 旋转视角</p>
  <p>🔄 滚轮: 缩放</p>
  <p>✨ 自动旋转: 开启</p>
</div>

<div class="footer">
  本文来源: <a href="https://blog.csdn.net/2302_80198073/article/details/143473333" target="_blank">前端小练习——星辰宇宙（JS没有上限！！！）</a>
</div>

<script type="module">
  // 导入三维模型库
  import * as THREE from "https://cdn.skypack.dev/three@0.136.0";
  // 导入轨道控制库
  import { OrbitControls } from "https://cdn.skypack.dev/three@0.136.0/examples/jsm/controls/OrbitControls";

  // 清除控制台
  console.clear();

  // 更新状态
  document.getElementById('status').textContent = '初始化中...';

  // 创建场景
  let scene = new THREE.Scene();
  // 设置场景背景颜色
  scene.background = new THREE.Color(0x160016);

  // 创建相机
  let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
  // 设置相机位置
  camera.position.set(0, 4, 21);

  // 创建渲染器
  let renderer = new THREE.WebGLRenderer({ antialias: true });
  // 设置渲染器大小
  renderer.setSize(innerWidth, innerHeight);
  // 把渲染器加入到页面中
  document.body.appendChild(renderer.domElement);

  // 监听窗口大小变化事件
  window.addEventListener("resize", event => {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });

  // 创建控制器
  let controls = new OrbitControls(camera, renderer.domElement);
  // 开启阻尼效果
  controls.enableDamping = true;
  // 禁用平移
  controls.enablePan = false;

  // 创建全局uniform
  let gu = {
    time: { value: 0 }
  };

  // 创建点的大小数组和移动数组
  let sizes = [];
  let shift = [];

  // 创建移动函数
  let pushShift = () => {
    shift.push(
            Math.random() * Math.PI,
            Math.random() * Math.PI * 2,
            (Math.random() * 0.9 + 0.1) * Math.PI * 0.1,
            Math.random() * 0.9 + 0.1
    );
  };

  console.log('开始生成粒子...');
  document.getElementById('status').textContent = '生成粒子中...';

  // 创建点的顶点数组（中间的球体）
  // 创建一个长度为5万的数组pts并用Array.prototype.map()方法遍历数组并对每个元素进行操作
  let pts = new Array(50000).fill().map(p => {
    // 每次遍历中，会向sizes数组中添加一个随机大小
    sizes.push(Math.random() * 1.5 + 0.5);
    // 调用pushShift()函数添加位置信息，并返回一个随机方向的THREE.Vector对象
    pushShift();
    return new THREE.Vector3().randomDirection().multiplyScalar(Math.random() * 0.5 + 9.5);
  });

  // 添加更多的点（旁边围绕的）
  // 先循环生成十万个点
  // 每次循环中生成一个随机数rand，再生成一个随机半径radius
  for (let i = 0; i < 100000; i++) {
    let r = 10, R = 40;
    let rand = Math.pow(Math.random(), 1.5);
    let radius = Math.sqrt(R * R * rand + (1 - rand) * r * r);
    // 使用new THREE.Vector3().setFromCylindricalCoords()生成一个点。
    pts.push(new THREE.Vector3().setFromCylindricalCoords(radius, Math.random() * 2 * Math.PI, (Math.random() - 0.5) * 2));
    sizes.push(Math.random() * 1.5 + 0.5);
    pushShift();
  }

  console.log(`生成了 ${pts.length} 个粒子`);
  document.getElementById('status').textContent = '创建几何体...';

  // 生成一个点g，同时将点的大小和位置信息添加到sizes和shift数组中
  let g = new THREE.BufferGeometry().setFromPoints(pts);
  // 创建了一个缓冲几何体并设置sizes和shift属性
  // 注意这里的F要大写Float32BufferAttribute
  g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1));
  g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4));

  document.getElementById('status').textContent = '创建材质...';

  // 创建点材质
  let m = new THREE.PointsMaterial({
    // 表示点的大小
    size: 0.125,
    // 设置材质为透明
    transparent: true,
    // 表示禁用深度测试，使点可以叠加
    depthTest: false,
    // 使用加法混合模式
    blending: THREE.AdditiveBlending,
    // 在材质编译之前修改着色器，在这里，它用来替换顶点着色器和片元着色器，添加uniform
    // 和attribute，以及自定义颜色和移动
    onBeforeCompile: shader => {
      shader.uniforms.time = gu.time;
      // 首先，它为着色器设置了一个uniform变量time，该变量是在点材质中定义的，用来追踪时间
      // 然后它定义了两个attribute变量sizes和shift，这两个变量是在缓冲几何体中定义的，用来控制点的大小和移动
      // 最后使用replace方法来替换顶点着色器中的代码
      shader.vertexShader = `
                    uniform float time;
                    attribute float sizes;
                    attribute vec4 shift;
                    varying vec3 vColor;
                    ${shader.vertexShader}
                `
              // 使用replace来替换着色器中的代码
              // 更新点的大小
              .replace(
                      `gl_PointSize = size;`,
                      `gl_PointSize = size * sizes;`
              )
              // 更新点的颜色
              .replace(
                      `#include <color_vertex>`,
                      `#include <color_vertex>
                    float d = length(abs(position)/vec3(40.,10.,40));
                    d=clamp(d,0.,1.);
                    vColor = mix(vec3(227.,155.,0.),vec3(100.,50.,255.),d)/255.;`
              )
              // 更新点的移动
              .replace(
                      `#include <begin_vertex>`,
                      `#include <begin_vertex>
                    float t = time;
                    float moveT = mod(shift.x + shift.z * t,PI2);
                    float moveS = mod(shift.y + shift.z * t,PI2);
                    transformed += vec3(cos(moveS) * sin(moveT),cos(moveT),sin(moveS)*sin(moveT)) * shift.w;`
              );

      // 修改片元着色器，用来让点的边缘更加圆滑
      // 首先，定义一个varying变量vColor，这个变量是在顶点着色器中定义的，用来传递点的颜色到片段着色器
      // 然后使用replace方法来替换片段着色器的代码
      shader.fragmentShader = `
                    varying vec3 vColor;
                    ${shader.fragmentShader}
                `.replace(
              `#include <clipping_planes_fragment>`,
              `#include <clipping_planes_fragment>
                    float d = length(gl_PointCoord.xy - 0.5);`
      ).replace(
              `vec4 diffuseColor = vec4( diffuse, opacity );`,
              `vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d));`
      );
    }
  });

  document.getElementById('status').textContent = '创建点云...';

  // 创建点云并将其添加到场景中，并设置渲染循环
  let p = new THREE.Points(g, m);
  // 旋转顺序为"ZYX"
  p.rotation.order = "ZYX";
  // 旋转角度 0.2
  p.rotation.z = 0.2;
  // 把对象（p）添加到场景（scene）中
  scene.add(p);

  document.getElementById('status').textContent = '启动渲染...';

  // 创建一个时钟对象clock
  let clock = new THREE.Clock();

  // 渲染循环，每次循环中会更新控制器，更新p的旋转角度，更新时间
  renderer.setAnimationLoop(() => {
    // 更新控制器
    controls.update();
    // 获取时钟对象（clock）的已经流逝的时间（t）并将他乘0.5
    let t = clock.getElapsedTime() * 0.5;
    // 将gu.time.value 设置为t*Math.PI
    gu.time.value = t * Math.PI;
    // 将对象（p）的旋转角度y设置为t*0.05
    p.rotation.y = t * 0.05;
    // 渲染场景（scene）和相机（camera）
    renderer.render(scene, camera);
  });

  // 初始化完成
  setTimeout(() => {
    document.getElementById('status').textContent = '运行中 ✨';
    console.log('星辰宇宙初始化完成！');
  }, 1000);

  // 添加一些交互提示
  let showTip = true;
  document.addEventListener('mousedown', () => {
    if (showTip) {
      console.log('🌟 尝试拖拽鼠标旋转视角，滚轮缩放！');
      showTip = false;
    }
  });
</script>
</body>
</html>